/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Haystack Software Inc. All rights reserved.
 *  Licensed under the PolyForm Strict License 1.0.0. See License.txt in the project root for
 *  license information.
 *--------------------------------------------------------------------------------------------*/

/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See code-license.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
// @ts-check
"use strict"
;(function () {
  /**
   * @param {number} value
   * @param {number} min
   * @param {number} max
   * @return {number}
   */
  function clamp(value, min, max) {
    return Math.min(Math.max(value, min), max)
  }

  function getSettings() {
    const element = document.getElementById("image-preview-settings")
    if (element) {
      const data = element.getAttribute("data-settings")
      if (data) {
        return JSON.parse(data)
      }
    }

    throw new Error(`Could not load settings`)
  }

  /**
   * Enable image-rendering: pixelated for images scaled by more than this.
   */
  const PIXELATION_THRESHOLD = 3

  const SCALE_PINCH_FACTOR = 0.075
  const MAX_SCALE = 20
  const MIN_SCALE = 0.1

  const zoomLevels = [
    0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1, 1.5, 2, 3, 5, 7, 10, 15, 20,
  ]

  const settings = getSettings()
  const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0

  // @ts-ignore
  const vscode = acquireVsCodeApi()

  const initialState = vscode.getState() || {
    scale: "fit",
    offsetX: 0,
    offsetY: 0,
  }

  // State
  let scale = initialState.scale
  let ctrlPressed = false
  let altPressed = false
  let hasLoadedImage = false
  let consumeClick = true
  let isActive = false

  // Elements
  const container = document.body
  const image = document.createElement("img")

  function updateScale(newScale) {
    if (!image || !hasLoadedImage || !image.parentElement) {
      return
    }

    if (newScale === "fit") {
      scale = "fit"
      image.classList.add("scale-to-fit")
      image.classList.remove("pixelated")
      // @ts-ignore Non-standard CSS property
      image.style.zoom = "normal"
      vscode.setState(undefined)
    } else {
      scale = clamp(newScale, MIN_SCALE, MAX_SCALE)
      if (scale >= PIXELATION_THRESHOLD) {
        image.classList.add("pixelated")
      } else {
        image.classList.remove("pixelated")
      }

      const dx =
        (window.scrollX + container.clientWidth / 2) / container.scrollWidth
      const dy =
        (window.scrollY + container.clientHeight / 2) / container.scrollHeight

      image.classList.remove("scale-to-fit")
      // @ts-ignore Non-standard CSS property
      image.style.zoom = scale

      const newScrollX = container.scrollWidth * dx - container.clientWidth / 2
      const newScrollY =
        container.scrollHeight * dy - container.clientHeight / 2

      window.scrollTo(newScrollX, newScrollY)

      vscode.setState({
        scale: scale,
        offsetX: newScrollX,
        offsetY: newScrollY,
      })
    }

    vscode.postMessage({
      type: "zoom",
      value: scale,
    })
  }

  function setActive(value) {
    isActive = value
    if (value) {
      if (isMac ? altPressed : ctrlPressed) {
        container.classList.remove("zoom-in")
        container.classList.add("zoom-out")
      } else {
        container.classList.remove("zoom-out")
        container.classList.add("zoom-in")
      }
    } else {
      ctrlPressed = false
      altPressed = false
      container.classList.remove("zoom-out")
      container.classList.remove("zoom-in")
    }
  }

  function firstZoom() {
    if (!image || !hasLoadedImage) {
      return
    }

    scale = image.clientWidth / image.naturalWidth
    updateScale(scale)
  }

  function zoomIn() {
    if (scale === "fit") {
      firstZoom()
    }

    let i = 0
    for (; i < zoomLevels.length; ++i) {
      if (zoomLevels[i] > scale) {
        break
      }
    }
    updateScale(zoomLevels[i] || MAX_SCALE)
  }

  function zoomOut() {
    if (scale === "fit") {
      firstZoom()
    }

    let i = zoomLevels.length - 1
    for (; i >= 0; --i) {
      if (zoomLevels[i] < scale) {
        break
      }
    }
    updateScale(zoomLevels[i] || MIN_SCALE)
  }

  window.addEventListener("keydown", (/** @type {KeyboardEvent} */ e) => {
    if (!image || !hasLoadedImage) {
      return
    }
    ctrlPressed = e.ctrlKey
    altPressed = e.altKey

    if (isMac ? altPressed : ctrlPressed) {
      container.classList.remove("zoom-in")
      container.classList.add("zoom-out")
    }
  })

  window.addEventListener("keyup", (/** @type {KeyboardEvent} */ e) => {
    if (!image || !hasLoadedImage) {
      return
    }

    ctrlPressed = e.ctrlKey
    altPressed = e.altKey

    if (!(isMac ? altPressed : ctrlPressed)) {
      container.classList.remove("zoom-out")
      container.classList.add("zoom-in")
    }
  })

  container.addEventListener("mousedown", (/** @type {MouseEvent} */ e) => {
    if (!image || !hasLoadedImage) {
      return
    }

    if (e.button !== 0) {
      return
    }

    ctrlPressed = e.ctrlKey
    altPressed = e.altKey

    consumeClick = !isActive
  })

  container.addEventListener("click", (/** @type {MouseEvent} */ e) => {
    if (!image || !hasLoadedImage) {
      return
    }

    if (e.button !== 0) {
      return
    }

    if (consumeClick) {
      consumeClick = false
      return
    }
    // left click
    if (scale === "fit") {
      firstZoom()
    }

    if (!(isMac ? altPressed : ctrlPressed)) {
      // zoom in
      zoomIn()
    } else {
      zoomOut()
    }
  })

  container.addEventListener(
    "wheel",
    (/** @type {WheelEvent} */ e) => {
      // Prevent pinch to zoom
      if (e.ctrlKey) {
        e.preventDefault()
      }

      if (!image || !hasLoadedImage) {
        return
      }

      const isScrollWheelKeyPressed = isMac ? altPressed : ctrlPressed
      if (!isScrollWheelKeyPressed && !e.ctrlKey) {
        // pinching is reported as scroll wheel + ctrl
        return
      }

      if (scale === "fit") {
        firstZoom()
      }

      const delta = e.deltaY > 0 ? 1 : -1
      updateScale(scale * (1 - delta * SCALE_PINCH_FACTOR))
    },
    { passive: false },
  )

  window.addEventListener(
    "scroll",
    (e) => {
      if (
        !image ||
        !hasLoadedImage ||
        !image.parentElement ||
        scale === "fit"
      ) {
        return
      }

      const entry = vscode.getState()
      if (entry) {
        vscode.setState({
          scale: entry.scale,
          offsetX: window.scrollX,
          offsetY: window.scrollY,
        })
      }
    },
    { passive: true },
  )

  container.classList.add("image")

  image.classList.add("scale-to-fit")

  image.addEventListener("load", () => {
    if (hasLoadedImage) {
      return
    }
    hasLoadedImage = true

    vscode.postMessage({
      type: "size",
      value: `${image.naturalWidth}x${image.naturalHeight}`,
    })

    document.body.classList.remove("loading")
    document.body.classList.add("ready")
    document.body.append(image)

    updateScale(scale)

    if (initialState.scale !== "fit") {
      window.scrollTo(initialState.offsetX, initialState.offsetY)
    }
  })

  image.addEventListener("error", (e) => {
    if (hasLoadedImage) {
      return
    }

    hasLoadedImage = true
    document.body.classList.add("error")
    document.body.classList.remove("loading")
  })

  image.src = settings.src

  document.querySelector(".open-file-link")?.addEventListener("click", (e) => {
    e.preventDefault()
    vscode.postMessage({
      type: "reopen-as-text",
    })
  })

  window.addEventListener("message", (e) => {
    if (e.origin !== window.origin) {
      console.error("Dropping message from unknown origin in image preview")
      return
    }

    switch (e.data.type) {
      case "setScale": {
        updateScale(e.data.scale)
        break
      }
      case "setActive": {
        setActive(e.data.value)
        break
      }
      case "zoomIn": {
        zoomIn()
        break
      }
      case "zoomOut": {
        zoomOut()
        break
      }
      case "copyImage": {
        copyImage()
        break
      }
    }
  })

  document.addEventListener("copy", () => {
    copyImage()
  })

  async function copyImage(retries = 5) {
    if (!document.hasFocus() && retries > 0) {
      // copyImage is called at the same time as webview.reveal, which means this function is running whilst the webview is gaining focus.
      // Since navigator.clipboard.write requires the document to be focused, we need to wait for focus.
      // We cannot use a listener, as there is a high chance the focus is gained during the setup of the listener resulting in us missing it.
      setTimeout(() => {
        copyImage(retries - 1)
      }, 20)
      return
    }

    try {
      await navigator.clipboard.write([
        new ClipboardItem({
          "image/png": new Promise((resolve, reject) => {
            const canvas = document.createElement("canvas")
            canvas.width = image.naturalWidth
            canvas.height = image.naturalHeight
            canvas.getContext("2d").drawImage(image, 0, 0)
            canvas.toBlob((blob) => {
              resolve(blob)
              canvas.remove()
            }, "image/png")
          }),
        }),
      ])
    } catch (e) {
      console.error(e)
    }
  }
})()
